31. 下一季度 TTM 股息率为正状态识别(K近邻算法)

本章概要

  • 学习材料:公司—季度财务特征与下一季度 TTM 股息率是否为正的状态标签。
  • 本章任务:运行 lst-ch31-local-panel 至季度前向调参和最终时间外测试,确保 shift 不跨公司。
  • 完成后你将得到:前向折时间边界、训练/测试期间、最优K、混淆矩阵和多数类基线。
  • 自我检查:抽一家公司核对标签对齐并比较基线;泄漏、失衡或测试期不足时 暂不采用模型。
  • 拓展练习:把标签拓展应用到同主体下一期逾期并重写FP/FN成本。

本节学习目标

  • 理解K近邻(KNN)算法的核心原理
  • 掌握欧氏距离的计算方法
  • 学会使用 scikit-learn 构建KNN分类模型
  • 理解网格搜索与交叉验证调优流程
  • 能够利用混淆矩阵评估分类模型性能

K近邻算法(KNN)是什么?

KNN 是一种经典的监督学习分类算法

  • 核心思想:‘物以类聚,人以群分’
  • 决策方式:根据K个最近邻居的类别进行多数投票
  • 不需要显式的模型训练过程,属于懒惰学习(Lazy Learning)

方法可拓展应用的应用(业务对象与标签必须重新验证):

  • 客户分类与画像
  • 信用评分与风险评估
  • 精准营销中的购买意愿预测

KNN算法原理:找到最近的K个邻居

决策规则

  1. 给定一个待预测样本 \(x\)
  2. 在训练集中找到与 \(x\) 距离最近\(K\) 个样本
  3. \(K\) 个邻居中出现次数最多的类别,即为 \(x\) 的预测类别

关键问题:如何衡量’距离’?

距离度量:欧氏距离

最常用的距离度量是欧氏距离(Euclidean Distance):

\[ \large d(x,y) = \sqrt{\sum_{i=1}^{n}(x_i - y_i)^2} \]

  • \(x_i, y_i\):两个样本在第 \(i\) 个特征上的取值
  • \(n\):特征总数

直观理解:就是多维空间中两点之间的直线距离

K值的选择至关重要

K值过小 K值过大
模型过拟合 模型欠拟合
对噪声敏感 决策边界过于平滑
分类不稳定 丢失局部特征

最佳实践:通过交叉验证自动搜索最优K值

受保护平台对照:广告购买预测

对照边界

  • 该客户级 CSV 由教学平台提供,用于学习 KNN 的基本用法。
  • 它不验证本章中国主案例,也不能支持 A 股 TTM 状态核对队列;主案例对象、标签和动作见后文。

数据特征

特征 说明
Age 用户年龄
EstimatedSalary 预估年薪
Purchased 是否购买(0/1)

运行前预测|平台任务代码

  • 输入预测:运行前先写出 dataxytransfer1 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到“预测结果:”相关结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:能独立说明本任务从输入到“平台任务代码”结果的关键步骤,原样录入平台代码并得到可核对的运行结果。

⭐ 平台任务代码

展开完整代码(投影默认折叠)
# 注:04_Social_Network_Ads.csv数据文件本地没有,但平台已经内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
# 导入相关模块
import pandas as pd
import numpy as np  # 导入NumPy数值计算库
from sklearn.model_selection import train_test_split  # 导入Scikit-learn的train_test_split模块
from sklearn.preprocessing import StandardScaler  # 导入Scikit-learn的StandardScaler模块
from sklearn.neighbors import KNeighborsClassifier  # 导入Scikit-learn的KNeighborsClassifier模块
from sklearn.model_selection import GridSearchCV  # 导入Scikit-learn的GridSearchCV模块
from sklearn.metrics import confusion_matrix  # 导入Scikit-learn的confusion_matrix模块
 
# 导入数据集
data = pd.read_csv("04_Social_Network_Ads.csv")
 
# 数据集划分
x = data[["Age","EstimatedSalary"]]
y = data["Purchased"]  # 提取Purchased列作为y变量
# 划分训练集和测试集
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.30, random_state=0)
 
# 数据标准化
transfer1 = StandardScaler()
x_train = transfer1.fit_transform(x_train)  # 对数据进行变换
x_test = transfer1.transform(x_test)  # 对数据进行变换
 
# 训练模型
estimator = KNeighborsClassifier(algorithm='kd_tree')
 
# 模型选择与调优——网格搜索和交叉验证
# 准备要调的超参数
param_dict = {"n_neighbors": [1, 3, 5, 7, 9, 11, 13]}
estimator = GridSearchCV(estimator, param_grid=param_dict, cv=4)  # 创建网格搜索交叉验证对象,自动寻找最优超参数
estimator.fit(x_train,y_train)  # 在数据上训练estimator模型
 
# 模型评估
y_pre = estimator.predict(x_test)
print("预测结果:\n", y_pre)  # 输出预测结果:\n
print("准确率为:\n", estimator.score(x_test, y_test))  # 输出准确率为:\n
# print("对比真实值和预测值:\n", y_test==y_pre)
print("在交叉验证中最好的结果为:\n", estimator.best_score_)
print("使用网格搜索的最好的模型:\n", estimator.best_estimator_)  # 输出使用网格搜索的最好的模型:\n

任务复盘|平台任务代码

运行后核对:核对 dataxytransfer1 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。

拓展练习:把对象或期间改为一家长三角上市公司或一组 A 股资产;先预测指标方向,再说明结果能支持和不能支持的决策。

代码解读:步骤一 — 数据导入与划分

数据导入:使用 pd.read_csv() 读取CSV文件

特征与标签分离

  • x:Age 和 EstimatedSalary(特征矩阵)
  • y:Purchased(目标变量)

数据集划分

  • 训练集占 70%,测试集占 30%
  • random_state=0 保证每次运行都能得到一致结果

代码解读:步骤二 — 数据标准化

为什么需要标准化?

  • 年龄范围:约 18~60
  • 薪资范围:约 15,000~150,000
  • 尺度差异巨大,距离计算会被薪资主导

StandardScaler 的作用

  • 将每个特征转换为均值为0、标准差为1的分布
  • fit_transform:在训练集上拟合并转换
  • transform:用训练集的参数转换测试集(避免数据泄露)

代码解读:步骤三 — 网格搜索与交叉验证

GridSearchCV 自动完成超参数调优:

  • 参数网格:K = [1, 3, 5, 7, 9, 11, 13]
  • 4折交叉验证:训练集分为4份,轮流验证
  • 自动找到使准确率最高的K值

平台数据 04_Social_Network_Ads.csv 仅用于教学平台固定练习;下方使用本地 A 股数据完整演示同一分析方法。

代码解读:步骤四 — 模型评估

评估指标

  • 准确率(Accuracy):预测正确的比例
  • 混淆矩阵(Confusion Matrix):详细展示分类结果

分类错误的业务代价并不对称(FP 与 FN 各自的代价因任务而异),下方真实数据处理过程会结合 TTM 股息率状态核对任务给出具体解读。

主案例混淆矩阵:下一季度 TTM 状态

预测:TTM股息率非正(0) 预测:TTM股息率为正(1)
实际:TTM股息率非正(0) TN(正确排除) FP(误纳入状态核对队列)
实际:TTM股息率为正(1) FN(漏掉核对对象) TP(正确纳入)

混淆矩阵:课堂动作边界

  • TN/TP:正确排除/纳入 TTM 状态核对队列。
  • FP:实际状态非正却纳入,增加人工核对工时。
  • FN:实际状态为正却漏掉,减少状态核对覆盖。
  • 该队列只用于课堂字段状态核对,不代表下一季度发生现金分红,也不是收益预测或投资建议。

真实数据验证:输入口径与判断条件

要素 课堂口径
本地资产 valuation_factors_quarterly_15_years.h5
样本/标签 2019Q3—2025Q3 日历对齐观测;标签=下一季度TTM股息率>0
特征 对数市值、市净率、市销率、盈利收益率
划分 有效训练至2023Q1;样本外自2024Q3(面板有季度缺口)
调优/参照 训练期内按季度扩展窗前向验证;最终时间外多数类基线

预声明性能判断条件

  • 基线增量:准确率超过多数类基线至少 3pp
  • 正状态召回:至少 85%
  • 正状态精度:至少 85%
  • 检查方法:KNN 和分组交叉验证均使用固定的数据划分;如果样本或字段不符合要求,应先检查数据再继续。

动作、成本与再次检查

  • 负责人:课程项目核对员。
  • 动作:判断条件通过后,只生成 TTM 状态核对队列。
  • 成本:记录“入队公司数 × 每家公司实际核对分钟数”,不预填成本。
  • 使用限制:如果没有事先说明名单容量和所需工时,就不输出名单。
  • 再次检查:每季末再次检查;下一次为 2026-09-30

真实数据验证:数据定位与日历对齐

Listing 1
展开数据定位与日历对齐代码
from pathlib import Path  # 导入路径工具以探测课程数据挂载
import numpy as np  # 导入数值计算库
import pandas as pd  # 导入数据分析库
from sklearn.preprocessing import StandardScaler  # 导入标准化器
from sklearn.neighbors import KNeighborsClassifier  # 导入KNN分类器
from sklearn.model_selection import GridSearchCV  # 导入网格搜索并接收自定义季度前向折
from sklearn.metrics import confusion_matrix, accuracy_score, recall_score, precision_score  # 导入评估指标
from sklearn.pipeline import Pipeline  # 导入管道以便在每个交叉验证训练折内拟合预处理
from sklearn.preprocessing import FunctionTransformer  # 导入无状态变换器以执行标准化后截尾
candidate_roots = [Path('/home/ubuntu/r2_data_mount/data'), Path(r'C:\qiufei\data')]  # 按课程规定顺序探测数据根目录
data_root = next((root for root in candidate_roots if root.is_dir()), None)  # 选择第一个可用根目录
factor_path = data_root / 'stock' / 'valuation_factors_quarterly_15_years.h5' if data_root else None  # 定位估值因子季度表
if factor_path is None or not factor_path.is_file():  # 真实数据缺失时明确提示
    raise FileNotFoundError(f'未找到估值因子文件:{factor_path};请从课程数据下载入口获取')  # 提示读者下载文件
Listing 2
展开公司季度日历对齐代码
valuation_panel = pd.read_hdf(factor_path, key='valuation_factors')  # 读取真实A股估值因子面板
feature_columns = ['market_cap', 'pb_ratio_lf', 'ps_ratio_ttm', 'ep_ratio_ttm']  # 规模与估值四特征
dividend_panel = valuation_panel[feature_columns + ['dividend_yield_ttm']].replace([np.inf, -np.inf], np.nan).dropna()  # 去除缺失与无穷值
dividend_panel = dividend_panel.reset_index().sort_values(['order_book_id', 'date'])  # 展开索引并按公司与报告期排序
calendar_quarters = pd.date_range(dividend_panel['date'].min(), dividend_panel['date'].max(), freq='QE')  # 构建完整日历季度网格
company_frames = []  # 收集每公司按日历重索引的结果
for company_code, company_frame in dividend_panel.groupby('order_book_id'):  # 逐公司处理
    reindexed_frame = company_frame.set_index('date').drop(columns='order_book_id').reindex(calendar_quarters)  # 把数据缺口显式补为缺失行
    reindexed_frame['order_book_id'] = company_code  # 回填公司代码
    company_frames.append(reindexed_frame.reset_index().rename(columns={'index': 'date'}))  # 收集结果
dividend_panel = pd.concat(company_frames, ignore_index=True)  # 合并为日历对齐面板
print(f'日历网格:{len(calendar_quarters)}个季度|覆盖公司:{dividend_panel["order_book_id"].nunique()}家')  # 报告日历对齐输入
日历网格:60个季度|覆盖公司:5157家

真实数据验证:标签构建与样本筛选

Listing 3
展开标签构建与样本筛选代码
next_quarter_yield = dividend_panel.groupby('order_book_id')['dividend_yield_ttm'].shift(-1)  # 同一公司下一日历季度TTM股息率
dividend_panel['next_ttm_yield_positive'] = next_quarter_yield > 0  # 标签:下一季度TTM股息率是否大于0
dividend_panel = dividend_panel[next_quarter_yield.notna()].dropna(subset=feature_columns)  # 仅保留标签与特征完整的行
classroom_panel = dividend_panel[dividend_panel['date'] >= '2019-01-01'].copy()  # 保留2019年起的课堂样本
classroom_panel['log_market_cap'] = np.log(classroom_panel['market_cap'])  # 市值对数化压缩尺度
print(f'输入口径:{len(classroom_panel)}个公司—季度样本({classroom_panel["date"].min().date()}{classroom_panel["date"].max().date()})|下一季度TTM股息率为正占比{classroom_panel["next_ttm_yield_positive"].mean():.1%}')  # 报告样本与标签分布
输入口径:76568个公司—季度样本(2019-09-30—2025-09-30)|下一季度TTM股息率为正占比76.4%

真实数据验证:时间划分与无泄漏预处理

Listing 4
展开时间有序划分与预处理管道代码
model_features = ['log_market_cap', 'pb_ratio_lf', 'ps_ratio_ttm', 'ep_ratio_ttm']  # 模型输入特征
train_mask = classroom_panel['date'] <= '2023-12-31'  # 名义训练期≤2023Q4;因面板季度缺失,有效标签止于2023Q1
test_mask = classroom_panel['date'] >= '2024-01-01'  # 名义测试期≥2024Q1;有效样本外观测自2024Q3起
x_train_raw = classroom_panel.loc[train_mask, model_features]  # 训练特征(标准化前)
x_test_raw = classroom_panel.loc[test_mask, model_features]  # 测试特征(标准化前)
y_train = classroom_panel.loc[train_mask, 'next_ttm_yield_positive']  # 提取训练期TTM股息率状态标签
y_test = classroom_panel.loc[test_mask, 'next_ttm_yield_positive']  # 提取样本外TTM股息率状态标签
majority_baseline = max(y_test.mean(), 1 - y_test.mean())  # 多数类基线准确率
print(f'时间划分:训练{len(y_train)}条(有效至2023Q1)|测试{len(y_test)}条(自2024Q3)|多数类基线{majority_baseline:.3f}|标准化将在每个训练折内拟合')  # 报告有效划分、基线与无泄漏口径
时间划分:训练51316条(有效至2023Q1)|测试25252条(自2024Q3)|多数类基线0.759|标准化将在每个训练折内拟合

真实数据验证:网格搜索调优

Listing 5
展开KNN网格搜索代码
clip_transformer = FunctionTransformer(np.clip, kw_args={'a_min': -5, 'a_max': 5})  # 将标准化后的极端值截到正负5且不估计额外参数
knn_pipeline = Pipeline([('scale', StandardScaler()), ('clip', clip_transformer), ('knn', KNeighborsClassifier())])  # 把标准化、截尾与KNN封装为同一管道
k_param_grid = {'knn__n_neighbors': [3, 5, 7, 9, 11, 13]}  # 使用管道参数名扫描候选K值
training_dates = classroom_panel.loc[train_mask, 'date'].reset_index(drop=True)  # 保存每条训练样本的季度键
candidate_validation_dates = sorted(training_dates.unique())[-4:]  # 取训练期末四个可用季度作前向验证块
forward_splits = []  # 收集严格早期训练到后期验证的索引对
for validation_date in candidate_validation_dates:  # 逐季度建立扩展窗验证折
    fold_train = np.flatnonzero(training_dates.lt(validation_date).to_numpy())  # 只允许更早季度进入折内训练
    fold_valid = np.flatnonzero(training_dates.eq(validation_date).to_numpy())  # 同一季度公司共同进入验证块
    if len(fold_train) and len(fold_valid): forward_splits.append((fold_train, fold_valid))  # 保存有效时间折
if len(forward_splits) < 3: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 前向折不足时停止调参
grid_search = GridSearchCV(knn_pipeline, param_grid=k_param_grid, cv=forward_splits, n_jobs=1)  # 每折只用更早季度拟合预处理与KNN
grid_search.fit(x_train_raw.reset_index(drop=True), y_train.reset_index(drop=True))  # 最终时间外测试集不参与调优
print(f'最优K={grid_search.best_params_["knn__n_neighbors"]}|季度前向{len(forward_splits)}折准确率{grid_search.best_score_:.3f}')  # 展示前向调优结果
最优K=13|季度前向4折准确率0.821

复算要求:旧的按公司随机分折可能用较晚季度验证较早季度,不再作为无时间泄漏依据。必须重新运行季度前向折并报告每折训练截止/验证季度;未执行时不预填最优K、CV分数或最终判断条件。

真实数据验证:样本外评估与判断条件

Listing 6
展开样本外评估与检查要求代码
y_predict = grid_search.predict(x_test_raw)  # 管道用训练期参数转换并预测样本外TTM股息率状态
confusion_table = confusion_matrix(y_test, y_predict)  # 混淆矩阵(行=实际,列=预测)
test_accuracy = accuracy_score(y_test, y_predict)  # 样本外准确率
positive_status_recall = recall_score(y_test, y_predict)  # 正状态召回率:真实TTM股息率为正中被找出的比例
positive_status_precision = precision_score(y_test, y_predict)  # 正状态精度:预测为正中真实为正的比例
improvement_pp = (test_accuracy - majority_baseline) * 100  # 相对基线的提升幅度(百分点)
margin_limit, recall_limit, precision_limit = 3.0, 0.85, 0.85  # 预声明的三条检查要求阈值
conditions_met = improvement_pp >= margin_limit and positive_status_recall >= recall_limit and positive_status_precision >= precision_limit  # 三条件同时满足才通过
decision = '允许生成课堂TTM状态核对队列' if conditions_met else '检查要求未通过,暂停状态核对队列输出'  # 课堂建议
print(f'混淆矩阵[[TN,FP],[FN,TP]]:{confusion_table.tolist()}')  # 展示分类明细
print(f'样本外准确率{test_accuracy:.3f}(基线{majority_baseline:.3f},+{improvement_pp:.1f}pp)|正状态召回{positive_status_recall:.1%}|正状态精度{positive_status_precision:.1%}')  # 展示验证指标
print(f'检查要求(优于基线≥{margin_limit:.0f}pp、召回≥{recall_limit:.0%}、精度≥{precision_limit:.0%}):{"通过" if conditions_met else "暂停"}{decision}')  # 展示检查要求与条件动作
混淆矩阵[[TN,FP],[FN,TP]]:[[4076, 2020], [2048, 17108]]
样本外准确率0.839(基线0.759,+8.0pp)|正状态召回89.3%|正状态精度89.4%
检查要求(优于基线≥3pp、召回≥85%、精度≥85%):通过|允许生成课堂TTM状态核对队列

真实数据验证:结论边界

  • 标签为“下一季度TTM股息率>0”的滚动状态(约四分之三为回溯四季成分),不是下一季度现金分红公告或支付事件;状态持续性使多数类基线较强。
  • 课堂代价:FP 增加人工状态核对工时;FN 减少核对覆盖。实际工时必须按队列逐项记录,不预填成本数据。
  • 最优K在网格边界(13),更大K更平滑;课堂网格与平台示例同量级,不外推。
  • 名单只用于课堂练习,不构成派息预测或投资建议;每季度末应重新查看样本期、因子、判断标准、名单容量和所需工时,条件发生变化时应重新分析。

本节总结

步骤 关键要点
划分 平台70/30随机划分;真实数据分析按时间有序划分
标准化 只用训练集拟合,防尺度主导与泄露
调优 训练期内按季度扩展窗前向验证搜索K值
评估 准确率+混淆矩阵,对比多数类基线

核心收获:特征标准化、K值选择和样本外验证是成败关键;结论须待预声明判断条件通过。

随堂练习

  • 问题 1|怎样完成本章分析?:独立运行 lst-ch31-local-panel 至时间切分,写出公司—季度标签对齐、各前向折训练截止/验证季度、最终训练/测试期间、最优K、混淆矩阵及多数类基线;抽一家公司核对 shift(-1) 没有跨主体。
  • 问题 2|结果说明什么?:用 2—3 句话解释距离投票为何可用于下一季度 TTM 股息率状态核对,并指出状态持续性、尺度敏感或类别失衡中的一个误判条件;明确它不等于现金分红事件预测。
  • 问题 3|换一个情境,怎样继续应用?:将标签改为同一主体下一期是否逾期,重新定义 FP/FN 业务成本,保持同主体标签、季度前向调参、折内缩放、时间外测试与多数类基线;写出一条 shift(-1) 不跨主体检查。
  • 作答提示:请完成三道题,并在回答中引用实际运行结果;拓展练习中不要把课堂示例写成普遍规律或因果结论。

教师参考解答|下一季度 TTM 状态

教师参考解答|答案与说明 1

  • 所用数据与字段valuation_factors_quarterly_15_years.h5:valuation_factors;公司—季度键,特征 market_cap, pb_ratio_lf, ps_ratio_ttm, ep_ratio_ttm;标签为同一公司下一日历季度 dividend_yield_ttm>0,不是下一季度现金分红事件。
  • 完整代码:先运行本章 lst-ch31-local-panellst-ch31-local-split 得到原始训练/测试表,再执行无泄漏管道:

教师参考解答|代码 1

展开代码(代码区可独立滚动)
from pathlib import Path  # 导入路径工具以定位规定估值因子快照
import numpy as np  # 导入数值工具以处理无穷值与对数市值
import pandas as pd  # 导入表格工具以对齐公司日历季度
factor_path=Path('/home/ubuntu/r2_data_mount/data/stock/valuation_factors_quarterly_15_years.h5')  # 绑定真实季度估值因子面板
if not factor_path.exists(): raise FileNotFoundError('未找到课程数据文件,请从课程数据下载入口获取并核对文件位置')  # 规定因子资产缺失时终止
valuation_panel=pd.read_hdf(factor_path,key='valuation_factors')  # 读取公司季度估值因子面板
feature_columns=['market_cap','pb_ratio_lf','ps_ratio_ttm','ep_ratio_ttm']  # 固定规模与估值四个预测特征
dividend_panel=valuation_panel[feature_columns+['dividend_yield_ttm']].replace([np.inf,-np.inf],np.nan).dropna().reset_index().sort_values(['order_book_id','date'])  # 去除非有限值并展开公司季度键
calendar_quarters=pd.date_range(dividend_panel['date'].min(),dividend_panel['date'].max(),freq='QE')  # 构造完整日历季度网格
company_frames=[]  # 初始化逐公司日历对齐结果
for company_code,company_frame in dividend_panel.groupby('order_book_id'):  # 逐公司隔离时序标签
    reindexed_frame=company_frame.set_index('date').drop(columns='order_book_id').reindex(calendar_quarters)  # 将缺失季度显式补为空行
    reindexed_frame['order_book_id']=company_code  # 回填公司代码避免跨主体移位
    company_frames.append(reindexed_frame.reset_index().rename(columns={'index':'date'}))  # 收集日历对齐的公司面板
dividend_panel=pd.concat(company_frames,ignore_index=True)  # 合并全部公司的日历季度表

教师参考解答|代码 2

展开代码(代码区可独立滚动)
next_quarter_yield=dividend_panel.groupby('order_book_id')['dividend_yield_ttm'].shift(-1)  # 仅在同一公司内取下一日历季度股息率
dividend_panel['next_ttm_yield_positive']=next_quarter_yield.gt(0)  # 构造下一季度TTM股息率大于零的状态标签
dividend_panel=dividend_panel.loc[next_quarter_yield.notna()].dropna(subset=feature_columns)  # 仅保留标签与特征完整样本
classroom_panel=dividend_panel.loc[dividend_panel['date'].ge('2019-01-01')].copy()  # 固定2019年起的课堂样本期
classroom_panel['log_market_cap']=np.log(classroom_panel['market_cap'])  # 对数化市值以缓和尺度偏斜
model_features=['log_market_cap','pb_ratio_lf','ps_ratio_ttm','ep_ratio_ttm']  # 固定进入KNN管道的特征顺序
train_mask=classroom_panel['date'].le('2023-12-31')  # 锁定2023年末前为训练期
test_mask=classroom_panel['date'].ge('2024-01-01')  # 锁定2024年起为时间外测试期
x_train_raw=classroom_panel.loc[train_mask,model_features]  # 提取未标准化的训练特征
x_test_raw=classroom_panel.loc[test_mask,model_features]  # 提取未标准化的样本外特征
y_train=classroom_panel.loc[train_mask,'next_ttm_yield_positive']  # 提取训练期下一季度TTM状态标签
y_test=classroom_panel.loc[test_mask,'next_ttm_yield_positive']  # 提取样本外下一季度TTM状态标签
if y_train.empty or y_test.empty or y_test.nunique()<2: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 训练测试或测试类别不完整时终止

教师参考解答|代码 3

展开代码(代码区可独立滚动)
from sklearn.pipeline import Pipeline  # 导入管道以封装无泄漏预处理与模型
from sklearn.preprocessing import StandardScaler  # 导入标准化器以仅在训练样本拟合尺度参数
from sklearn.neighbors import KNeighborsClassifier  # 导入K近邻分类器以执行距离投票
from sklearn.model_selection import GridSearchCV  # 导入网格搜索并使用自定义季度前向折
from sklearn.metrics import confusion_matrix,accuracy_score,precision_score,recall_score  # 导入分类或聚类指标以评价样本外结果
knn_pipeline=Pipeline([('scale',StandardScaler()),('knn',KNeighborsClassifier())])  # 导入标准化器以仅在训练样本拟合尺度参数
training_dates=classroom_panel.loc[train_mask,'date'].reset_index(drop=True)  # 保存训练样本季度以建立前向折
validation_dates=sorted(training_dates.unique())[-4:]  # 事先设定训练期末四个验证季度
forward_splits=[(np.flatnonzero(training_dates.lt(date)),np.flatnonzero(training_dates.eq(date))) for date in validation_dates]  # 每折只以更早季度训练并按整季度验证
forward_splits=[split for split in forward_splits if len(split[0]) and len(split[1])]  # 移除无训练或无验证样本的折
if len(forward_splits)<3: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 前向验证折不足时停止
grid_search=GridSearchCV(knn_pipeline,{'knn__n_neighbors':[3,5,7,9,11,13]},cv=forward_splits)  # 让折内缩放和KNN严格按时间调参
grid_search.fit(x_train_raw.reset_index(drop=True),y_train.reset_index(drop=True))  # 不让最终时间外测试参与调参
predicted_labels=grid_search.predict(x_test_raw); majority_baseline=max(y_test.mean(),1-y_test.mean())  # 按主体分组交叉验证选择邻居数
classification_metrics={'accuracy':accuracy_score(y_test,predicted_labels),'precision':precision_score(y_test,predicted_labels),'recall':recall_score(y_test,predicted_labels)}  # 生成时间外分类预测
adoption_conditions_met=classification_metrics['accuracy']-majority_baseline>=.03 and classification_metrics['precision']>=.85 and classification_metrics['recall']>=.85  # 合并分离度、稳定性与最小簇占比检查要求
print(grid_search.best_params_,confusion_matrix(y_test,predicted_labels),majority_baseline,classification_metrics,adoption_conditions_met)  # 合并分离度、稳定性与最小簇占比检查要求

教师参考解答|答案与说明 2

  • 解释答案:KNN 依据标准化特征空间中邻近样本投票;TTM 状态持续性会抬高多数类基线,因此只有时间外准确率、精度和召回同时满足要求才可生成课堂状态核对队列。该标签不表示下一季度发生现金分红。
  • 拓展应用答案:拓展应用到逾期识别时,把标签改为同一主体下一期是否逾期并重定义 FP/FN 成本,保持季度前向折内拟合缩放器和最终时间外多数类基线比较。
  • 参考结果:每折训练截止/验证季度、最优K、[[TN,FP],[FN,TP]]、多数类基线、样本外准确率/精度/召回与判断条件。另行核对:逐公司检查标签 shift(-1) 不跨公司,并用多数类常数预测手算基线。
  • 常见错误:TTM 状态持续性会抬高基线;状态队列不是现金派息或收益预测;不得随机拆面板、跨公司移位、用后期季度验证早期季度或把广告客户代价套到公司队列。